ns('J3D').F = 
  (function() {
     var currying = function(f, argumentCount) {
       return (
         function curryer (parms) {
           return function() {
             var args = parms.concat(Array.prototype.slice.call(arguments));
             if (argumentCount > args.length) {
               return curryer(args);
             } else {
               return f.apply(null, args);
             }
           };
         })([]);
     };
     

     /**
      * Construct a ordered two elements list. The element could be a pair, too.
      * @param {Object} x
      * @param {Object} y
      * @return {Pair}
      */
     var pair = currying(
       function(x, y) {
         return [x, y];
       }, 2);
     
     /**
      * Get the first element of a pair.
      * @param {Pair} z
      * @return {Object}
      */
     var fst = function(z) {
       return z[0];
     };
     
     /**
      * Get the second element of a pair.
      * @param {Pair} z
      * @return {Object}
      */
     var snd = function(z) {
       return z[1];
     };
     
     var isPair = function(e) {
       return typeof e === 'object' && e.length === 2;
     };
     
     var list = function() {
       return (function(args, i) {
         return i === args.length ? 
           null : pair(args[i], arguments.callee(args, i + 1));
       })(arguments, 0);
     };
     
     var listToStr = function(l) {
       return '[' + reduce(function(e1, e2) {
           e1 = isPair(e1) ? listToStr(e1) : e1;
           e2 = isPair(e2) ? listToStr(e2) : e2;
           return e1 + ' ' + e2;
       }, '')(l) + ']';
     };
     
     var listEqual = function(l1, l2) {
       if (l1 == null && l2 == null) {
         return true;
       } else if (l1 ==null || l2 == null) {
         return false;
       } else {
         return !(fst(l1) !== fst(l2) || !arguments.callee(snd(l1), snd(l2)));
       }
     };
     
     var delay = function(expr) {
       return function() {
         return expr;
       };
     };
     
     var force = function(de) {
       return de();
     };
     
     var stream = currying(function(x, y) {
       return [x, delay(y)];
     }, 2);
     
     var fstStream = function(z) {
       return fst(z);
     };
     
     var sndStream = function(z) {
       return force(snd(z));
     };
     
     var foldr = currying(function(f, x) {
       return function(p) {
         return p === null ? x : f(fst(p))(arguments.callee(snd(p)));
       };
     }, 2);
     
     var combine = function(f) {
       return currying(function(list1, list2) {
         return list1 == null || list2 == null ? null : 
           pair(f(fst(list1), fst(list2)),
                arguments.callee(snd(list1), snd(list2)));
       }, 2);
     };
     
     var filter = function(f) {
       return function(l) {
         if (l === null) {
           return null;
         } else {
           return f(fst(l)) ? 
             pair(fst(l), arguments.callee(snd(l))) : 
             arguments.callee(snd(l));
         }
       };
     };
     
     var reduce = currying(function(f, x) {
       return function(p) {
           return p === null ? x : f(fst(p), arguments.callee(snd(p)));
       };
     }, 2);
     
     var foldl = currying(function(f, x) {
       return function(p) {
         return p === null ? x : f(arguments.callee(snd(p)))(fst(p));
       };
     }, 2);
     
     var map = currying(function(f, list) {
       return foldr(compose(pair, f), null)(list);
     }, 2);
     
     var compose = currying(function(f, g) {
       return function(x) {
         return f(g(x));
       };
     }, 2);
     
     var and = currying(function(x, y) {
         return x && y;
     }, 2);
     
     var listCount = function(list) {
       return reduce(function(x, y) {
         return y + 1;
       }, 0)(list);
     };
     
     var memoize = function(func, context) {
       var memoizeArg = function(argPos) {
         var cache = {};
         return function() {
           if (argPos == 0) {
             if (!(arguments[argPos] in cache)) {
               cache[arguments[argPos]] = func.apply(context, arguments);
             }
             return cache[arguments[argPos]];
           } else {
             if (!(arguments[argPos] in cache)) {
               cache[arguments[argPos]] = memoizeArg(argPos - 1);
             }
             return cache[arguments[argPos]].apply(this, arguments);
           }
         };
       };
       var arity = func.arity || func.length;
       return memoizeArg(arity - 1);
     };
     
     return {
       pair: pair,
       fst: fst,
       snd: snd,
       list: list,
       listEqual: listEqual,
       listToStr: listToStr,
       listCount: listCount,
       stream: stream,
       fstStream: fstStream,
       sndStream: sndStream,
       filter: filter,
       foldr: foldr,
       reduce: reduce,
       map: map,
       foldl: foldl,
       currying: currying,
       compose: compose,
       combine: combine,
       plus: currying(function(x, y) {
         return x + y;
       }, 2),
       subtract: currying(function(x, y) {
         return x - y;
       }, 2),
       and: and,
       multiply: currying(function(x, y) {
         return x * y;
       }, 2),
       square: function(x) {
         return x * x;
       }
     };
   })();

